package com.db.williamchart.renderer;

import com.db.williamchart.extensions.Pair;
import com.db.williamchart.ChartContract;
import com.db.williamchart.Painter;
import com.db.williamchart.animation.ChartAnimation;
import com.db.williamchart.data.DataPoint;
import com.db.williamchart.data.Frame;
import com.db.williamchart.data.Label;
import com.db.williamchart.data.Paddings;
import com.db.williamchart.data.configuration.BarChartConfiguration;
import com.db.williamchart.data.configuration.ChartConfiguration;
import com.db.williamchart.extensions.ExtensionUtils;
import com.db.williamchart.extensions.ListUtils;
import com.db.williamchart.renderer.executor.DebugWithLabelsFrame;
import com.db.williamchart.renderer.executor.DefineHorizontalBarsClickableFrames;
import com.db.williamchart.renderer.executor.DefineVerticalBarsClickableFrames;
import com.db.williamchart.renderer.executor.GetHorizontalBarBackgroundFrames;
import com.db.williamchart.renderer.executor.GetHorizontalBarFrames;
import com.db.williamchart.renderer.executor.MeasureHorizontalBarChartPaddings;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import kotlin.Triple;

public class HorizontalBarChartRenderer implements ChartContract.Renderer {

    private List<DataPoint> data = new ArrayList<>();
    private float zeroPositionX = 0f;
    private Frame outerFrame;
    private Frame innerFrame;
    private BarChartConfiguration chartConfiguration;
    private List<Label> xLabels;
    private List<Label> yLabels;
    private List<Frame> barsBackgroundFrames;

    private ChartContract.BarView view;
    private Painter painter;
    private ChartAnimation<DataPoint> animation;

    public HorizontalBarChartRenderer(
            ChartContract.BarView view,
            Painter painter,
            ChartAnimation<DataPoint> animation) {

        this.view = view;
        this.painter = painter;
        this.animation = animation;
    }

    @Override

    public boolean preDraw(ChartConfiguration configuration) throws IllegalAccessException {
        if (data.isEmpty()) {
            return true;
        }

        chartConfiguration = (BarChartConfiguration) configuration;

        if (chartConfiguration.getScale().notInitialized()) {
            chartConfiguration = chartConfiguration.copy(ListUtils.toBarScale(data));
        }

        float scaleStep = chartConfiguration.getScale().getSize() / RendererConstants.defaultScaleNumberOfSteps;

        xLabels = new ArrayList<>();

        for (int i = 0; i < RendererConstants.defaultScaleNumberOfSteps + 1; i++) {
            float scaleValue = chartConfiguration.getScale().getMin() + scaleStep * i;
            chartConfiguration.setLabelsFormatter(scaleValue);
            Label label = new Label(
                    chartConfiguration.getLabelsFormatter(),
                    0,
                    0
            );
            xLabels.add(label);
        }

        yLabels = ListUtils.toLabels(data);
        Float yLongestChartLabelWidth = ExtensionUtils.maxValueBy(yLabels, new ExtensionUtils.Selector<Label, Float>() {
            @Override
            public Float doSelector(Label value) {
                return painter.measureLabelWidth(
                        value.getLabel(),
                        chartConfiguration.getLabelsSize()
                );
            }
        });

        if (yLongestChartLabelWidth == null) {
            throw new IllegalArgumentException("Looks like there's no labels to find the longest width.");
        }

        Paddings paddings = new MeasureHorizontalBarChartPaddings().invoke(
                chartConfiguration.getAxis(),
                painter.measureLabelHeight(chartConfiguration.getLabelsSize()),
                painter.measureLabelWidth(
                        xLabels.get(xLabels.size() - 1).getLabel(),
                        chartConfiguration.getLabelsSize()
                ),
                yLongestChartLabelWidth,
                RendererConstants.labelsPaddingToInnerChart
        );

        outerFrame = chartConfiguration.toOuterFrame();
        innerFrame = outerFrame.withPaddings(paddings);

        zeroPositionX = processZeroPositionX(
                innerFrame.getLeft(),
                innerFrame.getRight(),
                chartConfiguration.getScale().getSize()
        );

        placeLabelsX(innerFrame);
        placeLabelsY(outerFrame, innerFrame);
        placeDataPoints(innerFrame);

        barsBackgroundFrames = new GetHorizontalBarBackgroundFrames().invoke(
                innerFrame,
                chartConfiguration.getBarsSpacing(),
                data
        );

        animation.animateFrom(zeroPositionX, data, new ChartAnimation.AnimateFromCallback() {
            @Override
            public void callback() {
                view.postInvalidate();
            }
        });

        return false;
    }

    @Override
    public void draw() {
        if (data.isEmpty()) {
            return;
        }

        if (chartConfiguration.getAxis().shouldDisplayAxisX()) {
            view.drawLabels(xLabels);
        }

        if (chartConfiguration.getAxis().shouldDisplayAxisY()) {
            view.drawLabels(yLabels);
        }

        if (chartConfiguration.getBarsBackgroundColor() != -1) {
            view.drawBarsBackground(barsBackgroundFrames);
        }

        view.drawBars(
                new GetHorizontalBarFrames().invoke(
                        innerFrame,
                        zeroPositionX,
                        chartConfiguration.getBarsSpacing(),
                        data
                )
        );

        if (RendererConstants.inDebug) {

            List<Frame> list = Arrays.asList(outerFrame, innerFrame);
            list.addAll(
                    new DebugWithLabelsFrame().invoke(
                            painter,
                            chartConfiguration.getAxis(),
                            xLabels,
                            yLabels,
                            chartConfiguration.getLabelsSize()
                    )
            );
            List<Pair<Float, Float>> pairs = new ArrayList<>();
            for (DataPoint point : data) {
                pairs.add(new Pair<Float, Float>(point.getScreenPositionX(), point.getScreenPositionY()));
            }
            list.addAll(
                    new DefineHorizontalBarsClickableFrames().invoke(
                            innerFrame,
                            pairs
                    )
            );
            list.add(
                    new Frame(zeroPositionX, innerFrame.getTop(), zeroPositionX, innerFrame.getBottom())
            );

            view.drawDebugFrame(list);
        }
    }

    @Override
    public void render(List<Pair<String, Float>> entries) {
        data = ListUtils.toDataPoints(entries);
        view.postInvalidate();
    }

    @Override
    public void anim(List<Pair<String, Float>> entries, ChartAnimation<DataPoint> animation) {
        data = ListUtils.toDataPoints(entries);
        this.animation = animation;
        view.postInvalidate();
    }

    @Override
    public Triple<Integer, Float, Float> processClick(Float x, Float y) {
        if (x == null || y == null || data.isEmpty())
            return new Triple(-1, -1f, -1f);

        List<Pair<Float, Float>> pairList = new ArrayList<>();
        for (DataPoint point : data) {
            Pair<Float, Float> pair = new Pair<>(point.getScreenPositionX(), point.getScreenPositionY());
            pairList.add(pair);
        }
        List<Frame> frames = new DefineHorizontalBarsClickableFrames().invoke(
                innerFrame,
                pairList
        );
        int index = -1;
        for (int i = 0; i < frames.size(); i++) {
            Frame frame = frames.get(i);
            if (frame.contains(x, y)) {
                index = i;
                break;
            }
        }

        if (index != -1) {
            return new Triple<>(index,
                    data.get(index).getScreenPositionX(),
                    data.get(index).getScreenPositionY());
        } else {
            return new Triple<>(-1, -1f, -1f);
        }
    }

    @Override
    public Triple<Integer, Float, Float> processTouch(Float x, Float y) {
        return processClick(x, y);
    }

    private void placeLabelsX(Frame innerFrame) {
        float widthBetweenLabels =
                (innerFrame.getRight() - innerFrame.getLeft()) / RendererConstants.defaultScaleNumberOfSteps;
        float xLabelsVerticalPosition =
                innerFrame.getBottom() -
                        painter.measureLabelAscent(chartConfiguration.getLabelsSize()) +
                        RendererConstants.labelsPaddingToInnerChart;
        for (int i = 0; i < xLabels.size(); i++) {
            Label label = xLabels.get(i);
            label.setScreenPositionX(innerFrame.getLeft() + widthBetweenLabels * i);
            label.setScreenPositionY(xLabelsVerticalPosition);
        }
    }

    private void placeLabelsY(Frame outerFrame,Frame innerFrame) {
        float halfBarWidth = (innerFrame.getBottom() - innerFrame.getTop()) / yLabels.size() / 2;
        float labelsTopPosition = innerFrame.getTop() + halfBarWidth;
        float labelsBottomPosition = innerFrame.getBottom() - halfBarWidth;
        float heightBetweenLabels = (labelsBottomPosition - labelsTopPosition) / (yLabels.size() - 1);

        for (int i = 0; i < yLabels.size(); i++) {
            Label label = yLabels.get(i);
            label.setScreenPositionX(
                    outerFrame.getLeft() +
                            painter.measureLabelWidth(label.getLabel(), chartConfiguration.getLabelsSize()) / 2
            );
            label.setScreenPositionY(
                    labelsBottomPosition -
                            heightBetweenLabels * i +
                            painter.measureLabelDescent(chartConfiguration.getLabelsSize())
            );

        }

    }

    private void placeDataPoints(Frame innerFrame) {
        // Chart upper part with positive points
        float positiveWidth = innerFrame.getRight() - zeroPositionX;
        float positiveScale = chartConfiguration.getScale().getMax();

        // Chart bottom part with negative points
        float negativeWidth = zeroPositionX - innerFrame.getLeft();
        float negativeScale = chartConfiguration.getScale().getMin();

        float halfBarWidth = (innerFrame.getBottom() - innerFrame.getTop()) / yLabels.size() / 2;
        float labelsBottomPosition = innerFrame.getBottom() - halfBarWidth;
        float labelsTopPosition = innerFrame.getTop() + halfBarWidth;
        float heightBetweenLabels = (labelsBottomPosition - labelsTopPosition) / (yLabels.size() - 1);

        for (int i = 0; i < data.size(); i++) {
            DataPoint dataPoint = data.get(i);
            if (dataPoint.getValue() >= 0) {
                dataPoint.setScreenPositionX(
                        zeroPositionX + (positiveWidth * dataPoint.getValue() / positiveScale) // Positive
                );
            } else {
                dataPoint.setScreenPositionX(
                        zeroPositionX - (negativeWidth * dataPoint.getValue() / negativeScale) // Negative
                );
            }

            dataPoint.setScreenPositionY(labelsBottomPosition - heightBetweenLabels * i);

        }

    }

    private float processZeroPositionX(
            float innerLeft,
            float innerRight,
            float scaleRange
    ) {
        float chartWidth = innerRight - innerLeft;
        return innerRight - (chartWidth * chartConfiguration.getScale().getMax() / scaleRange);
    }

}
